﻿// -----------------------------------------------------------------------
// <copyright file="GoFiguration.cs" company="Eric Swanson">
// 
// SUMMARY: *Open source and free to use and modify! 
//          (including Commercially)
// 
// Copyright (c) 2012 Eric Swanson
// 
// Permission is hereby granted, free of charge, to any person obtaining 
// a copy of this software and associated documentation files (the 
// "Software"), to deal in the Software without restriction, including 
// without limitation the rights to use, copy, modify, merge, publish, 
// distribute, sublicense, and/or sell copies of the Software, and to 
// permit persons to whom the Software is furnished to do so, subject to 
// the following conditions:
// 
// The above copyright notice and this permission notice shall be 
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 
// BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
// SOFTWARE.
// </copyright>
// -----------------------------------------------------------------------

namespace GoFiguration

open System
open System.Collections.Generic
open System.ServiceModel
open Microsoft.FSharp.Core

#if DOTNET
open System.Configuration
#endif

[<AbstractClass>]
type GoFigurationBase() =
   
    /// <summary>Determines if a setting key exists in the current configuration.</summary>
    /// <param name="key">The setting key.</param>
    /// <returns><c>true</c> if a matching setting was found in the current configuration, otherwise <c>false</c>.</returns>
    abstract member ContainsKey : string -> bool
    
    /// <summary>Retrieves the value for the setting using the specified <paramref name="key"/>.</summary>
    /// <typeparam name="T">The type of the setting value expected.</typeparam>
    /// <param name="key">The setting key.</param>
    /// <returns>The setting value.</returns>
    member x.Get<'T>(key:string) = 
        x.Get<'T>(key, Unchecked.defaultof<'T>)

    /// <summary>Retrieves the value for the setting using the specified key.</summary>
    /// <typeparam name="T">The type of the setting value expected.</typeparam>
    /// <param name="key">The setting key.</param>
    /// <param name="defaultValue">The default value if the setting is not found.</param>
    /// <returns>The setting value.</returns>
    abstract member Get<'T> : string * 'T -> 'T
    
    /// <summary>Sets the value of the setting identified by the specified key.</summary>
    /// <typeparam name="T">The type of the setting value.</typeparam>
    /// <param name="key">The setting key.</param>
    /// <param name="value">The setting value.</param>
    /// <returns>The current instance of the GoFiguration object.</returns>
    abstract member Set<'T> : string * 'T -> GoFigurationBase

#if DOTNET
/// <summary>
/// Configuration provider for local &lt;appSettings /&gt;.
/// </summary>
type LocalAppSettingsGoFiguration() =
    inherit GoFigurationBase()
    
    let prepareKey (key:string) = key.Replace("appSettings:", "")
    
    /// <summary>
    /// Determines if a setting key exists in the current configuration.
    /// </summary>
    /// <param name="key">The setting key.</param>
    /// <returns>
    /// <c>true</c> if a matching setting was found in the current configuration, otherwise <c>false</c>.
    /// </returns>
    override x.ContainsKey(key:string) = 
        match ConfigurationManager.AppSettings.[prepareKey key] with
        | null -> false
        | _ -> true
        
    /// <summary>
    /// Retrieves the value for the setting using the specified key.
    /// </summary>
    /// <typeparam name="T">The type of the setting value expected.</typeparam>
    /// <param name="key">The setting key.</param>
    /// <param name="defaultValue">The default value if the setting is not found.</param>
    /// <returns>
    /// The setting value.
    /// </returns>
    override x.Get<'T>(key:string, defaultValue:'T) = 
        match x.ContainsKey(key) with
        | true -> box ConfigurationManager.AppSettings.[prepareKey key] :?> 'T
        | _ -> defaultValue
        
    /// <summary>
    /// Sets the value of the setting identified by the specified key.
    /// </summary>
    /// <typeparam name="T">The type of the setting value.</typeparam>
    /// <param name="key">The setting key.</param>
    /// <param name="value">The setting value.</param>
    /// <returns>
    /// The current instance of the GoFiguration object.
    /// </returns>
    override x.Set<'T>(key:string, value:'T) = 
        raise (InvalidOperationException("Local configuration application settings cannot be modified."))
        
/// <summary>
/// Configuration provider for local &lt;connectionStrings /&gt;.
/// </summary>
type LocalConnectionStringsGoFiguration() =
    inherit GoFigurationBase()
    
    let prepareKey (key:string) = key.Replace("connectionStrings:", "")
    
    /// <summary>
    /// Determines if a setting key exists in the current configuration.
    /// </summary>
    /// <param name="key">The setting key.</param>
    /// <returns>
    /// <c>true</c> if a matching setting was found in the current configuration, otherwise <c>false</c>.
    /// </returns>
    override x.ContainsKey(key:string) = 
        match ConfigurationManager.ConnectionStrings.[prepareKey key] with
        | null -> false
        | _ -> true
        
    /// <summary>
    /// Retrieves the value for the setting using the specified key.
    /// </summary>
    /// <typeparam name="T">The type of the setting value expected.</typeparam>
    /// <param name="key">The setting key.</param>
    /// <param name="defaultValue">The default value if the setting is not found.</param>
    /// <returns>
    /// The setting value.
    /// </returns>
    override x.Get<'T>(key:string, defaultValue:'T) = 
        match x.ContainsKey(key) with
        | true -> box ConfigurationManager.ConnectionStrings.[prepareKey key].ConnectionString :?> 'T
        | _ -> defaultValue
        
    /// <summary>
    /// Sets the value of the setting identified by the specified key.
    /// </summary>
    /// <typeparam name="T">The type of the setting value.</typeparam>
    /// <param name="key">The setting key.</param>
    /// <param name="value">The setting value.</param>
    /// <returns>
    /// The current instance of the GoFiguration object.
    /// </returns>
    override x.Set<'T>(key:string, value:'T) = 
        raise (InvalidOperationException("Local configuration connection strings cannot be modified."))
#endif

/// <summary>Configuration provider for managing in-memory settings.</summary>
type InMemoryGoFiguration() =
    inherit GoFigurationBase()

    let settings = new Dictionary<String, Object>()
    
    /// <summary>Determines if a setting key exists in the current configuration.</summary>
    /// <param name="key">The setting key.</param>
    /// <returns><c>true</c> if a matching setting was found in the current configuration, otherwise <c>false</c>.</returns>
    override x.ContainsKey(key:string) = 
        settings.ContainsKey(key)
    
    /// <summary>Retrieves the value for the setting using the specified <paramref name="key"/>.</summary>
    /// <typeparam name="T">The type of the setting value expected.</typeparam>
    /// <param name="key">The setting key.</param>
    /// <param name="defaultValue">The default value if the setting is not found.</param>
    /// <returns>The setting value.</returns>
    override x.Get<'T>(key:string, defaultValue:'T) = 
        match x.ContainsKey(key) with
        | true -> settings.Item(key) :?> 'T
        | _ -> defaultValue
    
    /// <summary>Sets the value of the setting identified by the specified <paramref name="key"/>.</summary>
    /// <typeparam name="T">The type of the setting value.</typeparam>
    /// <param name="key">The setting key.</param>
    /// <param name="value">The setting value.</param>
    /// <returns>The current instance of the GoFiguration object.</returns>
    override x.Set<'T>(key:string, value:'T) = 
        match x.ContainsKey(key) with
        | true -> settings.Item(key) <- value
        | _ -> settings.Add(key, value)
        x :> GoFigurationBase

/// <summary>Thrown when a configuration provider could not be found.</summary>
exception ConfigurationProviderNotFoundException of string

/// <summary>Configuration provider that delegates management of settings to other providers via a routed lookup.</summary>
type RoutedGoFiguration(providerLookupStrategy:System.Func<string, GoFigurationBase>) =
    inherit GoFigurationBase()

    let providerLookup = providerLookupStrategy
    
    /// <summary>Determines if a setting key exists in the current configuration.</summary>
    /// <param name="key">The setting key.</param>
    /// <returns><c>true</c> if a matching setting was found in the current configuration, otherwise <c>false</c>.</returns>
    /// <exception cref="ConfigurationProviderNotFoundException">Thrown when a provider could not be found for the specified <paramref name="key" /></exception>
    override x.ContainsKey(key:string) = 
        let provider = providerLookup.Invoke(key)
        match box provider with
        | null -> raise (ConfigurationProviderNotFoundException(sprintf "Configuration Provider not found for key %s" key))
        | _ -> provider.ContainsKey(key)
    
    /// <summary>Retrieves the value for the setting using the specified <paramref name="key"/>.</summary>
    /// <typeparam name="T">The type of the setting value expected.</typeparam>
    /// <param name="key">The setting key.</param>
    /// <param name="defaultValue">The default value if the setting is not found.</param>
    /// <returns>The setting value.</returns>
    /// <exception cref="ConfigurationProviderNotFoundException">Thrown when a provider could not be found for the specified <paramref name="key" /></exception>
    override x.Get<'T>(key:string, defaultValue:'T) = 
        let provider = providerLookup.Invoke(key)
        match box provider with
        | null -> raise (ConfigurationProviderNotFoundException(sprintf "Configuration Provider not found for key %s" key))
        | _ -> provider.Get<'T>(key, defaultValue)
    
    /// <summary>Sets the value of the setting identified by the specified <paramref name="key"/>.</summary>
    /// <typeparam name="T">The type of the setting value.</typeparam>
    /// <param name="key">The setting key.</param>
    /// <param name="value">The setting value.</param>
    /// <exception cref="ConfigurationProviderNotFoundException">Thrown when a provider could not be found for the specified <paramref name="key" /></exception>
    /// <returns>The current instance of the GoFiguration object.</returns>
    override x.Set<'T>(key:string, value:'T) = 
        let provider = providerLookup.Invoke(key)
        match box provider with
        | null -> raise (ConfigurationProviderNotFoundException(sprintf "Configuration Provider not found for key %s" key))
        | _ -> provider.Set<'T>(key, value)

/// <summary>Provides simplified and abstracted access to configuration data management.</summary>
type public GoFigure private () =
    
    // thread-safe lock object for updating the provider
    static let providerLock = new Object()

    #if DOTNET
    // match string prefix
    static let (|Prefix|_|) (p:string) (s:string) =
        if s.StartsWith(p) then Some()
        else None

    // config provider for <appSettings>
    static let appSettingsProvider = new LocalAppSettingsGoFiguration() :> GoFigurationBase

    // config provider for <connectionStrings>
    static let connectionStringsProvider = new LocalConnectionStringsGoFiguration() :> GoFigurationBase
    
    // in-memory provider
    static let inMemoryProvider = new InMemoryGoFiguration() :> GoFigurationBase
    
    // provider lookup strategy
    static let providerLookupStrategy (key:string) =
        match key with
        // delegate <appSettings>
        | Prefix "appSettings:" rest -> appSettingsProvider
        // delegate <connectionStrings>
        | Prefix "connectionStrings:" rest -> connectionStringsProvider
        // fallback on in-memory
        | _ -> inMemoryProvider
    
    // factory for the default backing store for config info
    static let loadDefaultProvider = fun() -> new RoutedGoFiguration(fun(key:string) -> providerLookupStrategy(key)) :> GoFigurationBase

    #else
    
    // factory for the default backing store for config info
    static let loadDefaultProvider = fun() -> new InMemoryGoFiguration() :> GoFigurationBase

    #endif
    
    // the backing store for config info
    static let mutable provider = loadDefaultProvider()
    
    /// <summary>Loads the default GoFigure configuration provider.</summary>
    /// <returns>The default instance of the GoFiguration provider.</returns>
    static member LoadDefaultProvider () =
        lock providerLock (fun() -> provider <- loadDefaultProvider())
        provider
    
    /// <summary>Sets the configuration provider using the specified <paramref name="newProvider"/>.</summary>
    /// <param name="newProvider">The new configuration provider to use.</param>
    /// <returns>The current instance of the GoFiguration provider.</returns>
    static member GetConfigurationFrom newProvider =
        lock providerLock (fun() -> provider <- newProvider)
        provider
        
    /// <summary>Determines if a setting key exists in the current configuration.</summary>
    /// <param name="key">The setting key.</param>
    /// <returns><c>true</c> if a matching setting was found in the current configuration, otherwise <c>false</c>.</returns>
    static member ContainsKey key = provider.ContainsKey(key)
    
    /// <summary>Retrieves the value for the setting using the specified <paramref name="key"/>.</summary>
    /// <typeparam name="T">The type of the setting value expected.</typeparam>
    /// <param name="key">The setting key.</param>
    /// <returns>The setting value.</returns>
    static member Get<'T> key = provider.Get<'T>(key)
    
    /// <summary>Retrieves the value for the setting using the specified <paramref name="key"/>.</summary>
    /// <typeparam name="T">The type of the setting value expected.</typeparam>
    /// <param name="key">The setting key.</param>
    /// <param name="defaultValue">The default value if the setting is not found.</param>
    /// <returns>The setting value.</returns>
    static member Get<'T> (key, defaultValue) = provider.Get<'T>(key, defaultValue)
    
    /// <summary>Sets the value of the setting identified by the specified <paramref name="key"/>.</summary>
    /// <typeparam name="T">The type of the setting value.</typeparam>
    /// <param name="key">The setting key.</param>
    /// <param name="value">The setting value.</param>
    /// <returns>The current instance of the GoFiguration object.</returns>
    static member Set<'T> (key, value) = provider.Set<'T>(key, value)
    
#if DOTNET

    /// <summary>Retrieves the value for the application setting using the specified <paramref name="key"/>.</summary>
    /// <param name="key">The application setting key.</param>
    /// <returns>The application setting value.</returns>
    static member GetAppSetting (key) =
        let fullKey = "appSettings:" + key
        provider.Get<string>(fullKey)

    /// <summary>Retrieves the value for the application setting using the specified <paramref name="key"/>.</summary>
    /// <param name="key">The application setting key.</param>
    /// <param name="defaultValue">The default value if the application setting is not found.</param>
    /// <returns>The application setting value.</returns>
    static member GetAppSetting (key, defaultValue) =
        let fullKey = "appSettings:" + key
        provider.Get<string>(fullKey, defaultValue)
        
    /// <summary>Retrieves the value for the connection string using the specified <paramref name="key"/>.</summary>
    /// <param name="key">The connection string key.</param>
    /// <returns>The connection string value.</returns>
    static member GetConnectionString (key) =
        let fullKey = "connectionStrings:" + key
        provider.Get<string>(fullKey)

    /// <summary>Retrieves the value for the connection string using the specified <paramref name="key"/>.</summary>
    /// <param name="key">The connection string key.</param>
    /// <param name="defaultValue">The default value if the connection string is not found.</param>
    /// <returns>The connection string value.</returns>
    static member GetConnectionString (key, defaultValue) =
        let fullKey = "connectionStrings:" + key
        provider.Get<string>(fullKey, defaultValue)

#endif

namespace GoFiguration.Services.Client

open System.Diagnostics
open System.ServiceModel
open System.ServiceModel.Channels

#if SILVERLIGHT
open System
open System.ComponentModel
open System.Net
open System.Threading
#endif

/// <summary>
/// Contract for a GoFigure WCF service.
/// </summary>
[<ServiceContract(ConfigurationName = "GoFigure.IGoFigureService", Namespace = "http://gofigure.codeplex.com")>]
type IGoFigureService =

#if DOTNET

    /// <summary>
    /// Gets the configuration setting value as XML using the specified <paramref name="key"/>.
    /// </summary>
    /// <param name="key">The setting key.</param>
    /// <returns>The configuration setting value as an XML <see cref="System.String"/>.</returns>
    [<OperationContract(Action = "http://gofigure.codeplex.com/IGoFigureService/GetAsXML", ReplyAction = "http://gofigure.codeplex.com/IGoFigureService/GetAsXMLResponse")>]
    abstract member GetAsXML : key:string -> string

    /// <summary>
    /// Gets the configuration setting value as JSON using the specified <paramref name="key"/>.
    /// </summary>
    /// <param name="key">The setting key.</param>
    /// <returns>The configuration setting value as a JSON <see cref="System.String"/>.</returns>
    [<OperationContract(Action = "http://gofigure.codeplex.com/IGoFigureService/GetAsJSON", ReplyAction = "http://gofigure.codeplex.com/IGoFigureService/GetAsJSONResponse")>]
    abstract member GetAsJSON : key:string -> string

#endif

#if SILVERLIGHT
    /// <summary>
    /// Begins the get as XML.
    /// </summary>
    /// <param name="key">The key.</param>
    /// <param name="callback">The callback.</param>
    /// <param name="asyncState">State of the async.</param>
    /// <returns>The asynchronous result of the call.</returns>
    [<OperationContract(AsyncPattern = true, ReplyAction = "http://gofigure.codeplex.com/IGoFigureService/GetAsXMLResponse")>]
    abstract member BeginGetAsXML : key:string * callback:AsyncCallback * asyncState:obj -> IAsyncResult

    /// <summary>
    /// Ends the get as XML.
    /// </summary>
    /// <param name="result">The result.</param>
    /// <returns>The XML.</returns>
    abstract member EndGetAsXML : result:IAsyncResult -> string

    /// <summary>
    /// Begins the get as JSON.
    /// </summary>
    /// <param name="key">The key.</param>
    /// <param name="callback">The callback.</param>
    /// <param name="asyncState">State of the async.</param>
    /// <returns>The asynchronous result of the call.</returns>
    [<OperationContract(AsyncPattern = true, Action = "http://gofigure.codeplex.com/IGoFigureService/GetAsJSON", ReplyAction = "http://gofigure.codeplex.com/IGoFigureService/GetAsJSONResponse")>]
    abstract member BeginGetAsJSON : key:string * callback:AsyncCallback * asyncState:obj -> IAsyncResult

    /// <summary>
    /// Ends the get as JSON.
    /// </summary>
    /// <param name="result">The result.</param>
    /// <returns>The JSON.</returns>
    abstract member EndGetAsJSON : result:IAsyncResult -> string

#endif

/// <summary>
/// Contract for a GoFigure service channel.
/// </summary>
type IGoFigureServiceChannel =
    inherit IGoFigureService
    inherit IClientChannel

#if DOTNET

/// <summary>
/// GoFigure service client.
/// </summary>
[<DebuggerStepThrough>]
type GoFigureServiceClient =
    inherit ClientBase<IGoFigureService>
    
    /// <summary>
    /// Initializes a new instance of the <see cref="GoFigureServiceClient"/> class.
    /// </summary>
    new() = { inherit ClientBase<IGoFigureService>(); }
    
    /// <summary>
    /// Initializes a new instance of the <see cref="GoFigureServiceClient"/> class.
    /// </summary>
    /// <param name="endpointConfigurationName">Name of the endpoint configuration.</param>
    new (endpointConfigurationName:string) = { inherit ClientBase<IGoFigureService>(endpointConfigurationName); }

    /// <summary>
    /// Initializes a new instance of the <see cref="GoFigureServiceClient"/> class.
    /// </summary>
    /// <param name="endpointConfigurationName">Name of the endpoint configuration.</param>
    /// <param name="remoteAddress">The remote address.</param>
    new (endpointConfigurationName:string, remoteAddress:string) = { inherit ClientBase<IGoFigureService>(endpointConfigurationName, remoteAddress); }
    
    /// <summary>
    /// Initializes a new instance of the <see cref="GoFigureServiceClient"/> class.
    /// </summary>
    /// <param name="endpointConfigurationName">Name of the endpoint configuration.</param>
    /// <param name="remoteAddress">The remote address.</param>
    new (endpointConfigurationName:string, remoteAddress:EndpointAddress) = { inherit ClientBase<IGoFigureService>(endpointConfigurationName, remoteAddress); }
    
    /// <summary>
    /// Initializes a new instance of the <see cref="GoFigureServiceClient"/> class.
    /// </summary>
    /// <param name="binding">The binding.</param>
    /// <param name="remoteAddress">The remote address.</param>
    new (binding:Binding, remoteAddress:EndpointAddress) = { inherit ClientBase<IGoFigureService>(binding, remoteAddress); }
    
    /// <summary>
    /// Gets the configuration setting value as XML using the specified <paramref name="key"/>.
    /// </summary>
    /// <param name="key">The setting key.</param>
    /// <returns>
    /// The configuration setting value as an XML <see cref="System.String"/>.
    /// </returns>
    member this.GetAsXML key:string = base.Channel.GetAsXML(key)

    /// <summary>
    /// Gets the configuration setting value as JSON using the specified <paramref name="key"/>.
    /// </summary>
    /// <param name="key">The setting key.</param>
    /// <returns>
    /// The configuration setting value as a JSON <see cref="System.String"/>.
    /// </returns>
    member this.GetAsJSON key:string = base.Channel.GetAsJSON(key)

    interface IGoFigureService with

        /// <summary>
        /// Gets the configuration setting value as XML using the specified <paramref name="key"/>.
        /// </summary>
        /// <param name="key">The setting key.</param>
        /// <returns>
        /// The configuration setting value as an XML <see cref="System.String"/>.
        /// </returns>
        member this.GetAsXML key:string = this.GetAsXML(key)

        /// <summary>
        /// Gets the configuration setting value as JSON using the specified <paramref name="key"/>.
        /// </summary>
        /// <param name="key">The setting key.</param>
        /// <returns>
        /// The configuration setting value as a JSON <see cref="System.String"/>.
        /// </returns>
        member this.GetAsJSON key:string = this.GetAsJSON(key)

#endif

#if SILVERLIGHT

type GoFigureServiceFactory =
    
    static member Create endpointConfigurationName =
        let factory = new ChannelFactory<IGoFigureService>(endpointConfigurationName)
        factory.CreateChannel()
    
    static member Create (endpointConfigurationName:string, remoteAddress:EndpointAddress) =
        let factory = new ChannelFactory<IGoFigureService>(endpointConfigurationName, remoteAddress)
        factory.CreateChannel()
    
    static member Create (binding:Binding, endpointAddress:EndpointAddress) =
        let factory = new ChannelFactory<IGoFigureService>(binding, endpointAddress)
        factory.CreateChannel()

#endif